--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 39f08ee9c4b386a76adc83814c4f9ad864cb53e2
Parents : 1c2977f
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-16T12:44:04-05:00
feat: add API endpoints for managing Reticulum interface modules and implement corresponding frontend components
Changes
17 files changed, 2175 insertions(+), 1546 deletions(-)
Diff
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 52341376..44322f24 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index fa4ca6c7..b53e73af 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -6352,6 +6352,122 @@ class ReticulumMeshChat:
},
)
+ @routes.get("/api/v1/reticulum/interface-modules")
+ async def reticulum_interface_modules_list(_request):
+ from meshchatx.src.backend.interface_module_store import (
+ list_interface_modules,
+ )
+
+ try:
+ payload = list_interface_modules(self.reticulum_config_dir)
+ except ValueError as e:
+ return web.json_response({"message": str(e)}, status=400)
+ except Exception as e:
+ return web.json_response(
+ {"message": f"Failed to list interface modules: {e!s}"},
+ status=500,
+ )
+ return web.json_response(payload)
+
+ @routes.post("/api/v1/reticulum/interface-modules")
+ async def reticulum_interface_modules_install(request):
+ from meshchatx.src.backend.interface_module_store import (
+ install_interface_module,
+ )
+
+ try:
+ content_type = request.headers.get("Content-Type", "")
+ filename = None
+ data = b""
+ overwrite = False
+ if "multipart/form-data" in content_type:
+ reader = await request.multipart()
+ field = await reader.next()
+ while field is not None:
+ if field.name == "file":
+ filename = field.filename or filename
+ chunks = []
+ while True:
+ chunk = await field.read_chunk()
+ if not chunk:
+ break
+ chunks.append(chunk)
+ data = b"".join(chunks)
+ elif field.name == "overwrite":
+ overwrite = (await field.text()).strip().lower() in (
+ "1",
+ "true",
+ "yes",
+ "on",
+ )
+ elif field.name == "filename":
+ filename = (await field.text()).strip() or filename
+ field = await reader.next()
+ else:
+ body = await request.json()
+ filename = body.get("filename") or body.get("type")
+ raw = body.get("content") or body.get("data") or ""
+ if isinstance(raw, str):
+ try:
+ data = base64.b64decode(raw, validate=False)
+ except (binascii.Error, ValueError):
+ data = raw.encode("utf-8")
+ elif isinstance(raw, (bytes, bytearray)):
+ data = bytes(raw)
+ overwrite = bool(body.get("overwrite", False))
+ if not data:
+ return web.json_response(
+ {"message": "Interface module file is required"},
+ status=400,
+ )
+ result = install_interface_module(
+ self.reticulum_config_dir,
+ filename=filename,
+ data=data,
+ overwrite=overwrite,
+ )
+ return web.json_response(
+ {
+ "message": (
+ f"Installed {result['filename']}. "
+ "Reload Reticulum or restart MeshChatX to load it."
+ ),
+ **result,
+ },
+ )
+ except ValueError as e:
+ return web.json_response({"message": str(e)}, status=422)
+ except Exception as e:
+ return web.json_response(
+ {"message": f"Failed to install interface module: {e!s}"},
+ status=500,
+ )
+
+ @routes.delete("/api/v1/reticulum/interface-modules/{type_name}")
+ async def reticulum_interface_modules_delete(request):
+ from meshchatx.src.backend.interface_module_store import (
+ delete_interface_module,
+ )
+
+ type_name = request.match_info.get("type_name")
+ try:
+ result = delete_interface_module(self.reticulum_config_dir, type_name)
+ except FileNotFoundError as e:
+ return web.json_response({"message": str(e)}, status=404)
+ except ValueError as e:
+ return web.json_response({"message": str(e)}, status=422)
+ except Exception as e:
+ return web.json_response(
+ {"message": f"Failed to delete interface module: {e!s}"},
+ status=500,
+ )
+ return web.json_response(
+ {
+ "message": f"Deleted {result['filename']}",
+ **result,
+ },
+ )
+
# add reticulum interface
@routes.post("/api/v1/reticulum/interfaces/add")
async def reticulum_interfaces_add(request):
diff --git a/meshchatx/src/backend/interface_module_store.py b/meshchatx/src/backend/interface_module_store.py
new file mode 100644
index 00000000..b2f36a3f
--- /dev/null
+++ b/meshchatx/src/backend/interface_module_store.py
@@ -0,0 +1,153 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Install custom Reticulum interface modules into ``configdir/interfaces``."""
+
+from __future__ import annotations
+
+import contextlib
+import os
+import re
+import tempfile
+
+_MODULE_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+_MAX_MODULE_BYTES = 512 * 1024
+
+
+def interface_modules_dir(reticulum_config_dir: str | None) -> str:
+ """Return the RNS ``interfacepath`` directory for this MeshChatX instance."""
+ if not reticulum_config_dir:
+ raise ValueError("Reticulum config directory is not configured")
+ root = os.path.abspath(os.path.expanduser(str(reticulum_config_dir)))
+ return os.path.join(root, "interfaces")
+
+
+def sanitize_interface_module_stem(name: str | None) -> str | None:
+ """Return a safe TypeName stem, or None when the name is invalid."""
+ if not name or not isinstance(name, str):
+ return None
+ raw = name.strip()
+ if not raw or "/" in raw or "\\" in raw or ".." in raw:
+ return None
+ stem = os.path.basename(raw)
+ if stem.lower().endswith(".py"):
+ stem = stem[:-3]
+ if not _MODULE_NAME_RE.fullmatch(stem):
+ return None
+ return stem
+
+
+def validate_interface_module_source(data: bytes) -> str | None:
+ """Return an error message when module bytes are unsafe or incomplete."""
+ if not data:
+ return "Interface module file is empty"
+ if len(data) > _MAX_MODULE_BYTES:
+ return f"Interface module is too large (max {_MAX_MODULE_BYTES} bytes)"
+ try:
+ text = data.decode("utf-8")
+ except UnicodeDecodeError:
+ return "Interface module must be UTF-8 text"
+ if "\x00" in text:
+ return "Interface module must be plain text"
+ if "interface_class" not in text:
+ return (
+ "Interface module must define interface_class "
+ "(RNS loads TypeName.py and expects that name)"
+ )
+ return None
+
+
+def list_interface_modules(reticulum_config_dir: str | None) -> dict:
+ """List installed ``*.py`` modules under interfacepath."""
+ path = interface_modules_dir(reticulum_config_dir)
+ modules: list[dict] = []
+ if os.path.isdir(path):
+ for entry in sorted(os.listdir(path)):
+ if not entry.endswith(".py") or entry.startswith("."):
+ continue
+ stem = sanitize_interface_module_stem(entry)
+ if stem is None:
+ continue
+ full = os.path.join(path, entry)
+ if not os.path.isfile(full):
+ continue
+ try:
+ size = os.path.getsize(full)
+ except OSError:
+ size = 0
+ modules.append({"type": stem, "filename": entry, "size": size})
+ return {
+ "interfacepath": path,
+ "modules": modules,
+ }
+
+
+def install_interface_module(
+ reticulum_config_dir: str | None,
+ *,
+ filename: str | None,
+ data: bytes,
+ overwrite: bool = False,
+) -> dict:
+ """Write a custom interface module into interfacepath.
+
+ Returns a dict with ``type``, ``filename``, ``path``, and ``interfacepath``.
+ Raises ``ValueError`` on validation failures.
+ """
+ err = validate_interface_module_source(data)
+ if err:
+ raise ValueError(err)
+ stem = sanitize_interface_module_stem(filename)
+ if stem is None:
+ raise ValueError(
+ "Filename must be a Python identifier plus .py "
+ "(example: WeaveInterface.py)"
+ )
+ target_dir = interface_modules_dir(reticulum_config_dir)
+ os.makedirs(target_dir, mode=0o700, exist_ok=True)
+ target_name = f"{stem}.py"
+ target_path = os.path.join(target_dir, target_name)
+ if os.path.exists(target_path) and not overwrite:
+ raise ValueError(
+ f"{target_name} already exists. Re-upload with overwrite enabled "
+ "to replace it."
+ )
+ fd, tmp_path = tempfile.mkstemp(prefix=".iface_", suffix=".py", dir=target_dir)
+ try:
+ with os.fdopen(fd, "wb") as handle:
+ handle.write(data)
+ handle.flush()
+ os.fsync(handle.fileno())
+ os.replace(tmp_path, target_path)
+ except Exception:
+ with contextlib.suppress(OSError):
+ os.remove(tmp_path)
+ raise
+ with contextlib.suppress(OSError):
+ os.chmod(target_path, 0o600)
+ return {
+ "type": stem,
+ "filename": target_name,
+ "path": target_path,
+ "interfacepath": target_dir,
+ "size": len(data),
+ }
+
+
+def delete_interface_module(
+ reticulum_config_dir: str | None,
+ type_name: str | None,
+) -> dict:
+ """Delete an installed interface module by type stem."""
+ stem = sanitize_interface_module_stem(type_name)
+ if stem is None:
+ raise ValueError("Invalid interface module type name")
+ target_dir = interface_modules_dir(reticulum_config_dir)
+ target_path = os.path.join(target_dir, f"{stem}.py")
+ if not os.path.isfile(target_path):
+ raise FileNotFoundError(f"{stem}.py is not installed")
+ os.remove(target_path)
+ return {
+ "type": stem,
+ "filename": f"{stem}.py",
+ "interfacepath": target_dir,
+ }
diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
index 743f2a11..aaced62f 100644
--- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
+++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
@@ -1303,6 +1303,78 @@
<p class="text-xs text-gray-600 dark:text-zinc-400 leading-relaxed">
{{ $t("interfaces.custom_external_intro") }}
</p>
+ <div
+ class="rounded-xl border border-amber-200/80 dark:border-amber-900/40 bg-amber-50/40 dark:bg-amber-950/20 p-3 space-y-2"
+ >
+ <p class="text-xs text-amber-900 dark:text-amber-200/90 leading-relaxed">
+ {{ $t("interfaces.custom_external_install_intro") }}
+ </p>
+ <p
+ v-if="interfaceModulesPath"
+ class="text-[10px] font-mono text-amber-800/80 dark:text-amber-200/70 break-all"
+ >
+ {{ $t("interfaces.custom_external_interfacepath_label") }}:
+ {{ interfaceModulesPath }}
+ </p>
+ <div class="flex flex-wrap items-center gap-2">
+ <input
+ ref="interface-module-file-input"
+ type="file"
+ accept=".py,text/x-python,text/plain"
+ class="hidden"
+ @change="onInterfaceModuleFileSelected"
+ />
+ <button
+ type="button"
+ class="secondary-chip py-1.5! px-3! text-[10px]!"
+ :disabled="interfaceModuleBusy"
+ @click="pickInterfaceModuleFile"
+ >
+ {{ $t("interfaces.custom_external_install_button") }}
+ </button>
+ <label class="flex items-center gap-1.5 text-[10px] text-gray-600 dark:text-zinc-400">
+ <input
+ v-model="interfaceModuleOverwrite"
+ type="checkbox"
+ class="rounded-sm"
+ />
+ {{ $t("interfaces.custom_external_install_overwrite") }}
+ </label>
+ </div>
+ <ul
+ v-if="installedInterfaceModules.length"
+ class="text-[10px] font-mono text-gray-600 dark:text-zinc-400 space-y-1"
+ >
+ <li
+ v-for="mod in installedInterfaceModules"
+ :key="mod.filename"
+ class="flex items-center justify-between gap-2"
+ >
+ <button
+ type="button"
+ class="text-left hover:underline"
+ @click="customExternalTypeName = mod.type"
+ >
+ {{ mod.filename }}
+ </button>
+ <button
+ type="button"
+ class="text-red-500 hover:text-red-600 shrink-0"
+ :disabled="interfaceModuleBusy"
+ :title="$t('interfaces.custom_external_module_delete')"
+ @click="deleteInstalledInterfaceModule(mod.type)"
+ >
+ {{ $t("interfaces.custom_external_module_delete") }}
+ </button>
+ </li>
+ </ul>
+ <p
+ v-else
+ class="text-[10px] text-gray-500 dark:text-zinc-500"
+ >
+ {{ $t("interfaces.custom_external_modules_empty") }}
+ </p>
+ </div>
<div>
<FormLabel class="glass-label">{{
$t("interfaces.custom_external_type_label")
@@ -1893,6 +1965,10 @@ export default {
customExternalTypeName: "",
customExternalOptionsJson: "{}",
docsReticulumInterfacesOverview: RETICULUM_MANUAL_INTERFACES_OVERVIEW_REL,
+ interfaceModulesPath: "",
+ installedInterfaceModules: [],
+ interfaceModuleOverwrite: false,
+ interfaceModuleBusy: false,
config: null,
@@ -2137,6 +2213,11 @@ export default {
newInterfaceCodingRate: "updateRNodeCalculations",
newInterfaceTxpower: "updateRNodeCalculations",
"RNodeInterfaceLoRaParameters.antennaGain": "updateRNodeCalculations",
+ newInterfaceType(value) {
+ if (value === "__external__") {
+ this.loadInstalledInterfaceModules();
+ }
+ },
},
mounted() {
this.getConfig();
@@ -2146,6 +2227,9 @@ export default {
this.loadComports();
this.loadHostKernelInterfaces();
this.loadCommunityInterfaces();
+ if (this.newInterfaceType === "__external__") {
+ this.loadInstalledInterfaceModules();
+ }
// check if we are editing an interface
const interfaceName = this.$route.query.interface_name;
@@ -2160,6 +2244,82 @@ export default {
}
},
methods: {
+ async loadInstalledInterfaceModules() {
+ try {
+ const response = await window.api.get("/api/v1/reticulum/interface-modules");
+ this.interfaceModulesPath = response.data?.interfacepath || "";
+ this.installedInterfaceModules = Array.isArray(response.data?.modules)
+ ? response.data.modules
+ : [];
+ } catch (e) {
+ console.log(e);
+ this.interfaceModulesPath = "";
+ this.installedInterfaceModules = [];
+ }
+ },
+ pickInterfaceModuleFile() {
+ const input = this.$refs["interface-module-file-input"];
+ if (input) {
+ input.value = "";
+ input.click();
+ }
+ },
+ async onInterfaceModuleFileSelected(event) {
+ const file = event?.target?.files?.[0];
+ if (!file) {
+ return;
+ }
+ this.interfaceModuleBusy = true;
+ try {
+ const formData = new FormData();
+ formData.append("file", file, file.name);
+ if (this.interfaceModuleOverwrite) {
+ formData.append("overwrite", "1");
+ }
+ const response = await window.api.post("/api/v1/reticulum/interface-modules", formData);
+ const typeName = response.data?.type;
+ if (typeName) {
+ this.customExternalTypeName = typeName;
+ }
+ ToastUtils.success(
+ response.data?.message || this.$t("interfaces.custom_external_install_success")
+ );
+ await this.loadInstalledInterfaceModules();
+ } catch (e) {
+ const message =
+ e?.response?.data?.message || this.$t("interfaces.custom_external_install_failed");
+ ToastUtils.error(message);
+ } finally {
+ this.interfaceModuleBusy = false;
+ if (event?.target) {
+ event.target.value = "";
+ }
+ }
+ },
+ async deleteInstalledInterfaceModule(typeName) {
+ if (!typeName || this.interfaceModuleBusy) {
+ return;
+ }
+ this.interfaceModuleBusy = true;
+ try {
+ const response = await window.api.delete(
+ `/api/v1/reticulum/interface-modules/${encodeURIComponent(typeName)}`
+ );
+ ToastUtils.success(
+ response.data?.message || this.$t("interfaces.custom_external_module_deleted")
+ );
+ if (this.customExternalTypeName === typeName) {
+ this.customExternalTypeName = "";
+ }
+ await this.loadInstalledInterfaceModules();
+ } catch (e) {
+ const message =
+ e?.response?.data?.message || this.$t("interfaces.custom_external_module_delete_failed");
+ ToastUtils.error(message);
+ } finally {
+ this.interfaceModuleBusy = false;
+ }
+ },
async getConfig() {
try {
const response = await window.api.get(`/api/v1/config`);
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index e7f24a8d..9bc4597f 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -1275,6 +1275,16 @@
"loopback_local_docs_hint": "Siehe Kapitel Interfaces für unterstützte Typen.",
"loopback_local_docs_link": "Gebündeltes Reticulum-Handbuch öffnen",
"custom_external_intro": "Typen wie WeaveInterface oder eigene Klassen werden geladen, wenn Reticulum eine passende Moduldatei (TypeName.py) unter interfacepath mit interface_class findet (externer Loader in RNS). JSON-Optionen werden in den Interface-Abschnitt Ihrer Konfiguration übernommen.",
+ "custom_external_install_intro": "Unter Android ist Android/data in der Dateien-App oft nicht sichtbar. Mit Modul installieren kopieren Sie eine TypeName.py über den System-Dateiauswahl (Downloads, Drive, Bluetooth usw.) in den interfacepath dieser App.",
+ "custom_external_interfacepath_label": "Aktiver interfacepath",
+ "custom_external_install_button": "Modul installieren (.py)",
+ "custom_external_install_overwrite": "Vorhandene Datei überschreiben",
+ "custom_external_install_success": "Schnittstellenmodul installiert",
+ "custom_external_install_failed": "Schnittstellenmodul konnte nicht installiert werden",
+ "custom_external_modules_empty": "Noch keine eigenen Module installiert.",
+ "custom_external_module_delete": "Löschen",
+ "custom_external_module_deleted": "Schnittstellenmodul gelöscht",
+ "custom_external_module_delete_failed": "Schnittstellenmodul konnte nicht gelöscht werden",
"custom_external_type_label": "Interface-Typname",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Zusätzliche Optionen (JSON-Objekt)",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index d771c557..74906b99 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "See the Interfaces chapter for supported types.",
"loopback_local_docs_link": "Open bundled Reticulum manual",
"custom_external_intro": "Types such as WeaveInterface or your own class are loaded when Reticulum finds a matching module file (TypeName.py) under interfacepath and exposes interface_class (see RNS source). Options below are merged into the interface stanza in your config file.",
+ "custom_external_install_intro": "On Android, Android/data is often hidden from the Files app. Use Install module to copy a TypeName.py into this app interfacepath via the system file picker (Downloads, Drive, Bluetooth, etc.).",
+ "custom_external_interfacepath_label": "Active interfacepath",
+ "custom_external_install_button": "Install module (.py)",
+ "custom_external_install_overwrite": "Overwrite if present",
+ "custom_external_install_success": "Interface module installed",
+ "custom_external_install_failed": "Could not install interface module",
+ "custom_external_modules_empty": "No custom modules installed yet.",
+ "custom_external_module_delete": "Delete",
+ "custom_external_module_deleted": "Interface module deleted",
+ "custom_external_module_delete_failed": "Could not delete interface module",
"custom_external_type_label": "Interface type name",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Additional options (JSON object)",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 26e1b7aa..96c8edd0 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "Vea el capítulo Interfaces para tipos admitidos.",
"loopback_local_docs_link": "Abrir manual Reticulum incluido",
"custom_external_intro": "Tipos como WeaveInterface se cargan si Reticulum encuentra un archivo de módulo coincidente (TypeName.py) en interfacepath con interface_class (cargador externo RNS). Las opciones JSON se fusionan en la sección de interfaz de su archivo de configuración.",
+ "custom_external_install_intro": "En Android, Android/data suele estar oculto en la app Archivos. Use Instalar módulo para copiar un TypeName.py a la interfacepath de esta app con el selector del sistema (Descargas, Drive, Bluetooth, etc.).",
+ "custom_external_interfacepath_label": "interfacepath activa",
+ "custom_external_install_button": "Instalar módulo (.py)",
+ "custom_external_install_overwrite": "Sobrescribir si ya existe",
+ "custom_external_install_success": "Módulo de interfaz instalado",
+ "custom_external_install_failed": "No se pudo instalar el módulo de interfaz",
+ "custom_external_modules_empty": "Aún no hay módulos personalizados instalados.",
+ "custom_external_module_delete": "Eliminar",
+ "custom_external_module_deleted": "Módulo de interfaz eliminado",
+ "custom_external_module_delete_failed": "No se pudo eliminar el módulo de interfaz",
"custom_external_type_label": "Nombre del tipo de interfaz",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Opciones adicionales (objeto JSON)",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 89071df0..c48abf32 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "Tutustu tuettuihin tyyppeihin luvusta Interfaces.",
"loopback_local_docs_link": "Avaa paikallisen Reticulum-ohjekirjan",
"custom_external_intro": "Tyypit, kuten WeaveInterface tai oma luokkasi, ladataan, kun Reticulum löytää vastaavan moduulitiedoston (TypeName.py) interfacepath-polusta ja paljastaa interface_class (katso RNS-lähdekoodi). Alla olevat asetukset yhdistetään liittymästanzaan asetustiedostossasi.",
+ "custom_external_install_intro": "Androidilla Android/data on usein piilossa Tiedostot-sovellukselta. Käytä Asenna moduli -toimintoa kopioidaksesi TypeName.py-tiedoston tämän sovelluksen interfacepath-kansioon järjestelmän tiedostonvalitsimella (Lataukset, Drive, Bluetooth jne.).",
+ "custom_external_interfacepath_label": "Aktiivinen interfacepath",
+ "custom_external_install_button": "Asenna moduli (.py)",
+ "custom_external_install_overwrite": "Korvaa jos olemassa",
+ "custom_external_install_success": "Liitäntämoduli asennettu",
+ "custom_external_install_failed": "Liitäntämodulin asennus epäonnistui",
+ "custom_external_modules_empty": "Omia moduleita ei ole vielä asennettu.",
+ "custom_external_module_delete": "Poista",
+ "custom_external_module_deleted": "Liitäntämoduli poistettu",
+ "custom_external_module_delete_failed": "Liitäntämodulin poisto epäonnistui",
"custom_external_type_label": "Sovitintyypin nimi",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Lisävalinnat (JSON-objekti)",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index a0385f7d..0757a52d 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "Voir le chapitre Interfaces pour les types pris en charge.",
"loopback_local_docs_link": "Ouvrir le manuel Reticulum intégré",
"custom_external_intro": "Les types comme WeaveInterface sont chargés si Reticulum trouve un fichier module correspondant (TypeName.py) dans interfacepath avec interface_class (voir le chargeur externe RNS). Les options JSON sont fusionnées dans la section interface de votre configuration.",
+ "custom_external_install_intro": "Sous Android, Android/data est souvent masqué dans l'application Fichiers. Utilisez Installer le module pour copier un TypeName.py dans l'interfacepath de cette application via le sélecteur système (Téléchargements, Drive, Bluetooth, etc.).",
+ "custom_external_interfacepath_label": "interfacepath active",
+ "custom_external_install_button": "Installer le module (.py)",
+ "custom_external_install_overwrite": "Écraser s'il existe",
+ "custom_external_install_success": "Module d'interface installé",
+ "custom_external_install_failed": "Impossible d'installer le module d'interface",
+ "custom_external_modules_empty": "Aucun module personnalisé installé pour le moment.",
+ "custom_external_module_delete": "Supprimer",
+ "custom_external_module_deleted": "Module d'interface supprimé",
+ "custom_external_module_delete_failed": "Impossible de supprimer le module d'interface",
"custom_external_type_label": "Nom du type d'interface",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Options supplémentaires (objet JSON)",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 97c16933..f37f3bf0 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -1275,6 +1275,16 @@
"loopback_local_docs_hint": "Vedi il capitolo Interfaces per i tipi supportati.",
"loopback_local_docs_link": "Apri il manuale Reticulum incluso",
"custom_external_intro": "Tipi come WeaveInterface o personalizzati si caricano se Reticulum trova un file modulo corrispondente (TypeName.py) in interfacepath con interface_class (caricatore esterno RNS). Le opzioni JSON si uniscono alla sezione interface nel file di configurazione.",
+ "custom_external_install_intro": "Su Android, Android/data è spesso nascosto nell'app File. Usa Installa modulo per copiare un TypeName.py nell'interfacepath di questa app tramite il selettore di sistema (Download, Drive, Bluetooth, ecc.).",
+ "custom_external_interfacepath_label": "interfacepath attiva",
+ "custom_external_install_button": "Installa modulo (.py)",
+ "custom_external_install_overwrite": "Sovrascrivi se presente",
+ "custom_external_install_success": "Modulo interfaccia installato",
+ "custom_external_install_failed": "Impossibile installare il modulo interfaccia",
+ "custom_external_modules_empty": "Nessun modulo personalizzato installato.",
+ "custom_external_module_delete": "Elimina",
+ "custom_external_module_deleted": "Modulo interfaccia eliminato",
+ "custom_external_module_delete_failed": "Impossibile eliminare il modulo interfaccia",
"custom_external_type_label": "Nome tipo interfaccia",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Opzioni aggiuntive (oggetto JSON)",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 4ee2bdf7..2f82f63c 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "Zie het hoofdstuk Interfaces voor ondersteunde types.",
"loopback_local_docs_link": "Gebundelde Reticulum-handleiding openen",
"custom_external_intro": "Typen zoals WeaveInterface worden geladen als Reticulum een passend modulebestand (TypeName.py) in interfacepath vindt met interface_class (externe loader RNS). JSON-opties worden samengevoegd in het interface-gedeelte van uw configuratie.",
+ "custom_external_install_intro": "Op Android is Android/data vaak verborgen in de Bestanden-app. Gebruik Module installeren om een TypeName.py via de systeemkiezer (Downloads, Drive, Bluetooth, enz.) naar de interfacepath van deze app te kopiëren.",
+ "custom_external_interfacepath_label": "Actieve interfacepath",
+ "custom_external_install_button": "Module installeren (.py)",
+ "custom_external_install_overwrite": "Overschrijven indien aanwezig",
+ "custom_external_install_success": "Interfacemodule geïnstalleerd",
+ "custom_external_install_failed": "Interfacemodule kon niet worden geïnstalleerd",
+ "custom_external_modules_empty": "Nog geen aangepaste modules geïnstalleerd.",
+ "custom_external_module_delete": "Verwijderen",
+ "custom_external_module_deleted": "Interfacemodule verwijderd",
+ "custom_external_module_delete_failed": "Interfacemodule kon niet worden verwijderd",
"custom_external_type_label": "Interfacetype-naam",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Extra opties (JSON-object)",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index bf197fa5..416fcfa7 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -1275,6 +1275,16 @@
"loopback_local_docs_hint": "См. главу Interfaces о поддерживаемых типах.",
"loopback_local_docs_link": "Открыть встроенное руководство Reticulum",
"custom_external_intro": "Типы вроде WeaveInterface загружаются, если Reticulum находит подходящий файл модуля (TypeName.py) в interfacepath с interface_class (внешний загрузчик RNS). Поля JSON попадают в секцию interface в вашем конфиге.",
+ "custom_external_install_intro": "В Android папка Android/data часто скрыта в приложении «Файлы». Используйте «Установить модуль», чтобы скопировать TypeName.py в interfacepath этого приложения через системный выбор файлов (Загрузки, Диск, Bluetooth и т. д.).",
+ "custom_external_interfacepath_label": "Активный interfacepath",
+ "custom_external_install_button": "Установить модуль (.py)",
+ "custom_external_install_overwrite": "Перезаписать, если есть",
+ "custom_external_install_success": "Модуль интерфейса установлен",
+ "custom_external_install_failed": "Не удалось установить модуль интерфейса",
+ "custom_external_modules_empty": "Пользовательские модули ещё не установлены.",
+ "custom_external_module_delete": "Удалить",
+ "custom_external_module_deleted": "Модуль интерфейса удалён",
+ "custom_external_module_delete_failed": "Не удалось удалить модуль интерфейса",
"custom_external_type_label": "Имя типа интерфейса",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "Доп. параметры (JSON-объект)",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 0f6d22fa..59dd4174 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1223,6 +1223,16 @@
"loopback_local_docs_hint": "参阅 Interfaces 章节了解支持的类型。",
"loopback_local_docs_link": "打开内置 Reticulum 手册",
"custom_external_intro": "WeaveInterface 等类型会在 Reticulum 于 interfacepath 找到匹配的模块文件(TypeName.py)且包含 interface_class 时加载(RNS 外部加载器)。JSON 选项会合并到配置文件中的 interface 段。",
+ "custom_external_install_intro": "在 Android 上,文件应用经常无法打开 Android/data。请使用“安装模块”,通过系统文件选择器(下载、云盘、蓝牙等)将 TypeName.py 复制到本应用的 interfacepath。",
+ "custom_external_interfacepath_label": "当前 interfacepath",
+ "custom_external_install_button": "安装模块 (.py)",
+ "custom_external_install_overwrite": "若已存在则覆盖",
+ "custom_external_install_success": "接口模块已安装",
+ "custom_external_install_failed": "无法安装接口模块",
+ "custom_external_modules_empty": "尚未安装自定义模块。",
+ "custom_external_module_delete": "删除",
+ "custom_external_module_deleted": "接口模块已删除",
+ "custom_external_module_delete_failed": "无法删除接口模块",
"custom_external_type_label": "接口类型名称",
"custom_external_type_placeholder": "WeaveInterface",
"custom_external_json_label": "附加选项(JSON 对象)",
diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json
index f7a8cbe3..ae3ad3df 100644
--- a/tests/backend/fixtures/http_api_routes.json
+++ b/tests/backend/fixtures/http_api_routes.json
@@ -1,1548 +1,1560 @@
{
- "routes": [
- {
- "method": "GET",
- "path": "/"
- },
- {
- "method": "GET",
- "path": "/api/v1/announce"
- },
- {
- "method": "GET",
- "path": "/api/v1/announces"
- },
- {
- "method": "POST",
- "path": "/api/v1/announces/query"
- },
- {
- "method": "GET",
- "path": "/api/v1/app/changelog"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/changelog/seen"
- },
- {
- "method": "GET",
- "path": "/api/v1/app/info"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/integrity/acknowledge"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/shutdown"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/tutorial/seen"
- },
- {
- "method": "GET",
- "path": "/api/v1/auth/csrf"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/login"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/logout"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/setup"
- },
- {
- "method": "GET",
- "path": "/api/v1/auth/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/blocked-destinations"
- },
- {
- "method": "POST",
- "path": "/api/v1/blocked-destinations"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/blocked-destinations/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/announce"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/delete"
- },
- {
- "method": "GET",
- "path": "/api/v1/bots/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/restart"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/start"
- },
- {
- "method": "GET",
- "path": "/api/v1/bots/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/bots/subprocess-log"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/bots/update"
- },
- {
- "method": "GET",
- "path": "/api/v1/community-interfaces"
- },
- {
- "method": "POST",
- "path": "/api/v1/community-interfaces/refresh"
- },
- {
- "method": "GET",
- "path": "/api/v1/comports"
- },
- {
- "method": "GET",
- "path": "/api/v1/config"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/config"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/backup"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backup/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backups"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/database/backups/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backups/{filename}/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/health"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/recover"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/restore"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/snapshot"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/snapshots"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/database/snapshots/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/snapshots/{filename}/download"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/vacuum"
- },
- {
- "method": "GET",
- "path": "/api/v1/debug/access-attempts"
- },
- {
- "method": "GET",
- "path": "/api/v1/debug/logs"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/custom-display-name"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/custom-display-name/update"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/drop-path"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/lxmf-stamp-info"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/path"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/request-path"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/signal-metrics"
- },
- {
- "method": "GET",
- "path": "/api/v1/diagnostics/memory"
- },
- {
- "method": "GET",
- "path": "/api/v1/diagnostics/memory/gc"
- },
- {
- "method": "POST",
- "path": "/api/v1/diagnostics/memory/gc/collect"
- },
- {
- "method": "GET",
- "path": "/api/v1/diagnostics/memory/heap"
- },
- {
- "method": "GET",
- "path": "/api/v1/diagnostics/memory/referrers"
- },
- {
- "method": "POST",
- "path": "/api/v1/diagnostics/memory/reset"
- },
- {
- "method": "POST",
- "path": "/api/v1/diagnostics/memory/snapshot"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/export"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/export/reticulum"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/search"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/docs/switch"
- },
- {
- "method": "POST",
- "path": "/api/v1/docs/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/docs/version/{version}"
- },
- {
- "method": "GET",
- "path": "/api/v1/favourites"
- },
- {
- "method": "POST",
- "path": "/api/v1/favourites/add"
- },
- {
- "method": "POST",
- "path": "/api/v1/favourites/import"
- },
- {
- "method": "GET",
- "path": "/api/v1/favourites/layout"
- },
- {
- "method": "PUT",
- "path": "/api/v1/favourites/layout"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/favourites/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/favourites/{destination_hash}/rename"
- },
- {
- "method": "GET",
- "path": "/api/v1/gifs"
- },
- {
- "method": "POST",
- "path": "/api/v1/gifs"
- },
- {
- "method": "GET",
- "path": "/api/v1/gifs/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/gifs/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/gifs/{gif_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/gifs/{gif_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/gifs/{gif_id}/image"
- },
- {
- "method": "POST",
- "path": "/api/v1/gifs/{gif_id}/use"
- },
- {
- "method": "GET",
- "path": "/api/v1/identities"
- },
- {
- "method": "POST",
- "path": "/api/v1/identities/create"
- },
- {
- "method": "GET",
- "path": "/api/v1/identities/export-all"
- },
- {
- "method": "POST",
- "path": "/api/v1/identities/switch"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/identities/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/identity/backup/base32"
- },
- {
- "method": "GET",
- "path": "/api/v1/identity/backup/download"
- },
- {
- "method": "POST",
- "path": "/api/v1/identity/restore"
- },
- {
- "method": "GET",
- "path": "/api/v1/interface-stats"
- },
- {
- "method": "GET",
- "path": "/api/v1/licenses"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/reactions"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/send"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf-messages/{hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/{hash}/cancel"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/{hash}/spam"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/{message_hash}/uri"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/conversation-pins"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversation-pins/toggle"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/conversations"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/bulk-delete"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/bulk-mark-as-read"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/move-to-folder"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/{destination_hash}/mark-as-read"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/folders"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/folders"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/folders/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/folders/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf/folders/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/lxmf/folders/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/message-blocklist"
- },
- {
- "method": "PUT",
- "path": "/api/v1/lxmf/message-blocklist"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/message-blocklist/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/message-blocklist/import"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/propagation-node/restart"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/propagation-node/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/stop-sync"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/sync"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-nodes"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/sieve-filters"
- },
- {
- "method": "PUT",
- "path": "/api/v1/lxmf/sieve-filters"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/announces"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/archives"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/docs/reticulum"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/favourites"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/gifs"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/lxmf-icons"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/messages"
- },
- {
- "method": "GET",
- "path": "/api/v1/maintenance/messages/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/maintenance/messages/import"
- },
- {
- "method": "POST",
- "path": "/api/v1/maintenance/messages/import-file"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/path-table"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/stickers"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/drawings"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/drawings"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/drawings/{drawing_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/map/drawings/{drawing_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/export"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/export/{export_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/export/{export_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/export/{export_id}/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/mbtiles"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/mbtiles/active"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/mbtiles/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/offline"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/offline"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/overlays"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/overlays"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/overlays/export"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/overlays/jobs/{job_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/overlays/jobs/{job_id}/cancel"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/overlays/{overlay_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/map/overlays/{overlay_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/overlays/{overlay_id}/content"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/overlays/{overlay_id}/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/overlays/{overlay_id}/refresh"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/tiles/{z}/{x}/{y}"
- },
- {
- "method": "GET",
- "path": "/api/v1/meshchatx-docs/content"
- },
- {
- "method": "GET",
- "path": "/api/v1/meshchatx-docs/list"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/nomadnet/archives"
- },
- {
- "method": "GET",
- "path": "/api/v1/nomadnet/archives"
- },
- {
- "method": "POST",
- "path": "/api/v1/nomadnetwork/{destination_hash}/identify"
- },
- {
- "method": "GET",
- "path": "/api/v1/notification-sounds"
- },
- {
- "method": "GET",
- "path": "/api/v1/notification-sounds/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/notification-sounds/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/notification-sounds/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/notification-sounds/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/notification-sounds/{id}/audio"
- },
- {
- "method": "GET",
- "path": "/api/v1/notifications"
- },
- {
- "method": "POST",
- "path": "/api/v1/notifications/mark-as-viewed"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/announce"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/files"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/files"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}/files/{file_name}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/pages"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/pages"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
- },
- {
- "method": "PUT",
- "path": "/api/v1/page-nodes/{node_id}/rename"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/path-table"
- },
- {
- "method": "POST",
- "path": "/api/v1/path-table"
- },
- {
- "method": "GET",
- "path": "/api/v1/ping/{destination_hash}/lxmf.delivery"
- },
- {
- "method": "GET",
- "path": "/api/v1/plugins"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/install"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/preview"
- },
- {
- "method": "GET",
- "path": "/api/v1/plugins/trusted-publishers"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/trusted-publishers"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/plugins/trusted-publishers/{identity}"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/plugins/{plugin_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/plugins/{plugin_id}/asset/{asset_path:.*}"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/{plugin_id}/disable"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/{plugin_id}/enable"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/{plugin_id}/invoke"
- },
- {
- "method": "POST",
- "path": "/api/v1/plugins/{plugin_id}/report-failure"
- },
- {
- "method": "POST",
- "path": "/api/v1/repository-server/http/restart"
- },
- {
- "method": "POST",
- "path": "/api/v1/repository-server/http/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/repository-server/http/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/repository-server/list"
- },
- {
- "method": "POST",
- "path": "/api/v1/repository-server/refresh-bundled"
- },
- {
- "method": "GET",
- "path": "/api/v1/repository-server/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/repository-server/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/repository-server/upload/{name}"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/blackhole"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/config/raw"
- },
- {
- "method": "PUT",
- "path": "/api/v1/reticulum/config/raw"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/config/reset"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/disable-transport"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/discovered-interfaces"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/discovery"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/reticulum/discovery"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/enable-transport"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/instance"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/reticulum/instance"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/interfaces"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/add"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/bitrates"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/delete"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/disable"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/enable"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/import"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/import-preview"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/management-identities"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/management-identities"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/recover"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/reload"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/cancel"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/fetch"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/listen"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/send"
- },
- {
- "method": "GET",
- "path": "/api/v1/rncp/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/rncp/transfer/{transfer_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop-queues"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop-via"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/rates"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/request"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/table"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/trace/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnprobe"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnsh/sessions"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rnsh/sessions/{session_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions/{session_id}/clear"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions/{session_id}/input"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnsh/sessions/{session_id}/output"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions/{session_id}/resize"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions/{session_id}/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnsh/sessions/{session_id}/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnstatus"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnx/sessions"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rnx/sessions/{session_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions/{session_id}/clear"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions/{session_id}/input"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnx/sessions/{session_id}/output"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions/{session_id}/resize"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions/{session_id}/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnx/sessions/{session_id}/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/hubs"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs"
- },
- {
- "method": "PUT",
- "path": "/api/v1/rrc/hubs/order"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rrc/hubs/{hub_hash}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/rrc/hubs/{hub_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/command"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/connect"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/disconnect"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms"
- },
- {
- "method": "PUT",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/order"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/read"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/servers"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rrc/servers/{hub_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/rrc/servers/{hub_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/servers/{hub_id}/activity"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers/{hub_id}/announce"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/servers/{hub_id}/members"
- },
- {
- "method": "GET",
- "path": "/api/v1/rrc/servers/{hub_id}/messages"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers/{hub_id}/moderate"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers/{hub_id}/rooms"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/rrc/servers/{hub_id}/rooms/{room}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers/{hub_id}/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/rrc/servers/{hub_id}/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/self-test"
- },
- {
- "method": "GET",
- "path": "/api/v1/server/security"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/server/security"
- },
- {
- "method": "POST",
- "path": "/api/v1/setup/storage-migration"
- },
- {
- "method": "GET",
- "path": "/api/v1/sideband-plugins"
- },
- {
- "method": "GET",
- "path": "/api/v1/sideband-plugins/config"
- },
- {
- "method": "POST",
- "path": "/api/v1/sideband-plugins/config"
- },
- {
- "method": "POST",
- "path": "/api/v1/sideband-plugins/reload"
- },
- {
- "method": "GET",
- "path": "/api/v1/spam-keywords"
- },
- {
- "method": "POST",
- "path": "/api/v1/spam-keywords"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/spam-keywords/{keyword_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/sticker-packs"
- },
- {
- "method": "POST",
- "path": "/api/v1/sticker-packs"
- },
- {
- "method": "POST",
- "path": "/api/v1/sticker-packs/install"
- },
- {
- "method": "POST",
- "path": "/api/v1/sticker-packs/reorder"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/sticker-packs/{pack_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/sticker-packs/{pack_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/sticker-packs/{pack_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/sticker-packs/{pack_id}/export"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers"
- },
- {
- "method": "POST",
- "path": "/api/v1/stickers"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/stickers/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/stickers/{sticker_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/stickers/{sticker_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers/{sticker_id}/image"
- },
- {
- "method": "GET",
- "path": "/api/v1/system/network-interfaces"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/history/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/latest/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/peers"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/tracking"
- },
- {
- "method": "POST",
- "path": "/api/v1/telemetry/tracking/{destination_hash}/toggle"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/trusted-peers"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/answer"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/audio-profiles"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/call/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/codec2/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/contacts"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts/check/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/contacts/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/contacts/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/telephone/contacts/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/hangup"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/history"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/history"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/mute-receive"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/mute-transmit"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/recordings"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/recordings/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/recordings/{id}/audio/{side}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/ringtones/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/ringtones/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/telephone/ringtones/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones/{id}/audio"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/send-to-voicemail"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/switch-audio-profile/{profile_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/unmute-receive"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/unmute-transmit"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/generate-greeting"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/voicemail/greeting"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemail/greeting/audio"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/record/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/record/stop"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/upload"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemail/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemails"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/voicemails/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemails/{id}/audio"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemails/{id}/read"
- },
- {
- "method": "GET",
- "path": "/api/v1/tools/micron-parser-go-release"
- },
- {
- "method": "GET",
- "path": "/api/v1/tools/rnode/download_firmware"
- },
- {
- "method": "GET",
- "path": "/api/v1/tools/rnode/latest_release"
- },
- {
- "method": "POST",
- "path": "/api/v1/translator/install-languages"
- },
- {
- "method": "GET",
- "path": "/api/v1/translator/languages"
- },
- {
- "method": "POST",
- "path": "/api/v1/translator/translate"
- },
- {
- "method": "GET",
- "path": "/call.html"
- },
- {
- "method": "GET",
- "path": "/manifest.json"
- },
- {
- "method": "GET",
- "path": "/service-worker.js"
- },
- {
- "method": "GET",
- "path": "/ws"
- },
- {
- "method": "GET",
- "path": "/ws/telephone/audio"
- }
- ]
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/announce"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/announces"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/announces/query"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/app/changelog"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/changelog/seen"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/app/info"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/integrity/acknowledge"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/shutdown"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/tutorial/seen"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/auth/csrf"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/login"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/logout"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/setup"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/auth/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/blocked-destinations"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/blocked-destinations"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/blocked-destinations/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/announce"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/delete"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/bots/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/restart"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/start"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/bots/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/bots/subprocess-log"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/bots/update"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/community-interfaces"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/community-interfaces/refresh"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/comports"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/config"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/config"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/backup"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backup/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backups"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/database/backups/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backups/{filename}/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/health"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/recover"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/restore"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/snapshot"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/snapshots"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/database/snapshots/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/snapshots/{filename}/download"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/vacuum"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/debug/access-attempts"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/debug/logs"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/custom-display-name"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/custom-display-name/update"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/drop-path"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/lxmf-stamp-info"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/path"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/request-path"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/signal-metrics"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/diagnostics/memory"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/diagnostics/memory/gc"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/diagnostics/memory/gc/collect"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/diagnostics/memory/heap"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/diagnostics/memory/referrers"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/diagnostics/memory/reset"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/diagnostics/memory/snapshot"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/export"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/export/reticulum"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/search"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/docs/switch"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/docs/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/docs/version/{version}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/favourites"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/favourites/add"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/favourites/import"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/favourites/layout"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/favourites/layout"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/favourites/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/favourites/{destination_hash}/rename"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/gifs"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/gifs"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/gifs/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/gifs/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/gifs/{gif_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/gifs/{gif_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/gifs/{gif_id}/image"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/gifs/{gif_id}/use"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identities"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identities/create"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identities/export-all"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identities/switch"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/identities/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identity/backup/base32"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identity/backup/download"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identity/restore"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/interface-stats"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/licenses"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/reactions"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/send"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf-messages/{hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/{hash}/cancel"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/{hash}/spam"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/{message_hash}/uri"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/conversation-pins"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversation-pins/toggle"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/conversations"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/bulk-delete"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/bulk-mark-as-read"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/move-to-folder"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/{destination_hash}/mark-as-read"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/folders"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/folders"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/folders/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/folders/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf/folders/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/lxmf/folders/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/message-blocklist"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/lxmf/message-blocklist"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/message-blocklist/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/message-blocklist/import"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/propagation-node/restart"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/propagation-node/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/stop-sync"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/sync"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-nodes"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/sieve-filters"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/lxmf/sieve-filters"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/announces"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/archives"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/docs/reticulum"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/favourites"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/gifs"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/lxmf-icons"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/messages"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/maintenance/messages/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/maintenance/messages/import"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/maintenance/messages/import-file"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/path-table"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/stickers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/drawings"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/drawings"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/drawings/{drawing_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/map/drawings/{drawing_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/export"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/export/{export_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/export/{export_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/export/{export_id}/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/mbtiles"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/mbtiles/active"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/mbtiles/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/offline"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/offline"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/overlays"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/overlays"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/overlays/export"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/overlays/jobs/{job_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/overlays/jobs/{job_id}/cancel"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/overlays/{overlay_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/map/overlays/{overlay_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/overlays/{overlay_id}/content"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/overlays/{overlay_id}/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/overlays/{overlay_id}/refresh"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/tiles/{z}/{x}/{y}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/meshchatx-docs/content"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/meshchatx-docs/list"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/nomadnet/archives"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/nomadnet/archives"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/nomadnetwork/{destination_hash}/identify"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/notification-sounds"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/notification-sounds/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/notification-sounds/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/notification-sounds/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/notification-sounds/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/notification-sounds/{id}/audio"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/notifications"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/notifications/mark-as-viewed"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/announce"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/files"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/files"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}/files/{file_name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/pages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/pages"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/page-nodes/{node_id}/rename"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/path-table"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/path-table"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/ping/{destination_hash}/lxmf.delivery"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/plugins"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/install"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/preview"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/plugins/trusted-publishers"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/trusted-publishers"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/plugins/trusted-publishers/{identity}"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/plugins/{plugin_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/plugins/{plugin_id}/asset/{asset_path:.*}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/{plugin_id}/disable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/{plugin_id}/enable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/{plugin_id}/invoke"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/plugins/{plugin_id}/report-failure"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/repository-server/http/restart"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/repository-server/http/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/repository-server/http/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/repository-server/list"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/repository-server/refresh-bundled"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/repository-server/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/repository-server/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/repository-server/upload/{name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/blackhole"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/config/raw"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/reticulum/config/raw"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/config/reset"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/disable-transport"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/discovered-interfaces"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/discovery"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/reticulum/discovery"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/enable-transport"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/instance"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/reticulum/instance"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/interface-modules"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interface-modules"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/reticulum/interface-modules/{type_name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/interfaces"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/add"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/bitrates"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/delete"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/disable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/enable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/import"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/import-preview"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/management-identities"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/management-identities"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/recover"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/reload"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/cancel"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/fetch"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/listen"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/send"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rncp/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rncp/transfer/{transfer_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop-queues"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop-via"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/rates"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/request"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/table"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/trace/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnprobe"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnsh/sessions"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rnsh/sessions/{session_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions/{session_id}/clear"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions/{session_id}/input"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnsh/sessions/{session_id}/output"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions/{session_id}/resize"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions/{session_id}/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnsh/sessions/{session_id}/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnstatus"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnx/sessions"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rnx/sessions/{session_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions/{session_id}/clear"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions/{session_id}/input"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnx/sessions/{session_id}/output"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions/{session_id}/resize"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions/{session_id}/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnx/sessions/{session_id}/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/hubs"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/rrc/hubs/order"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/hubs/{hub_hash}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/rrc/hubs/{hub_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/command"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/connect"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/disconnect"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/order"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/messages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/hubs/{hub_hash}/rooms/{room}/read"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/servers"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/servers/{hub_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/rrc/servers/{hub_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/servers/{hub_id}/activity"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers/{hub_id}/announce"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/servers/{hub_id}/members"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rrc/servers/{hub_id}/messages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers/{hub_id}/moderate"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers/{hub_id}/rooms"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/rrc/servers/{hub_id}/rooms/{room}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers/{hub_id}/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rrc/servers/{hub_id}/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/self-test"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/server/security"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/server/security"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/setup/storage-migration"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/sideband-plugins"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/sideband-plugins/config"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/sideband-plugins/config"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/sideband-plugins/reload"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/spam-keywords"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/spam-keywords"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/spam-keywords/{keyword_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/sticker-packs"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/sticker-packs"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/sticker-packs/install"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/sticker-packs/reorder"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/sticker-packs/{pack_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/sticker-packs/{pack_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/sticker-packs/{pack_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/sticker-packs/{pack_id}/export"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/stickers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/stickers/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/stickers/{sticker_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/stickers/{sticker_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers/{sticker_id}/image"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/system/network-interfaces"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/history/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/latest/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/peers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/tracking"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telemetry/tracking/{destination_hash}/toggle"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/trusted-peers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/answer"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/audio-profiles"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/call/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/codec2/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/contacts"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts/check/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/contacts/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/contacts/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/telephone/contacts/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/hangup"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/history"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/history"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/mute-receive"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/mute-transmit"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/recordings"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/recordings/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/recordings/{id}/audio/{side}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/ringtones/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/ringtones/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/telephone/ringtones/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones/{id}/audio"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/send-to-voicemail"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/switch-audio-profile/{profile_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/unmute-receive"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/unmute-transmit"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/generate-greeting"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/voicemail/greeting"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemail/greeting/audio"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/record/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/record/stop"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/upload"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemail/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemails"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/voicemails/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemails/{id}/audio"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemails/{id}/read"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/tools/micron-parser-go-release"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/tools/rnode/download_firmware"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/tools/rnode/latest_release"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/translator/install-languages"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/translator/languages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/translator/translate"
+ },
+ {
+ "method": "GET",
+ "path": "/call.html"
+ },
+ {
+ "method": "GET",
+ "path": "/manifest.json"
+ },
+ {
+ "method": "GET",
+ "path": "/service-worker.js"
+ },
+ {
+ "method": "GET",
+ "path": "/ws"
+ },
+ {
+ "method": "GET",
+ "path": "/ws/telephone/audio"
+ }
+ ]
}
diff --git a/tests/backend/http_api_response_registry.py b/tests/backend/http_api_response_registry.py
index 7cc8ad6d..7be54859 100644
--- a/tests/backend/http_api_response_registry.py
+++ b/tests/backend/http_api_response_registry.py
@@ -146,6 +146,19 @@ HTTP_JSON_GET_CONTRACTS: tuple[HttpJsonContract, ...] = (
SYSTEM_NETWORK_INTERFACES_SCHEMA,
),
HttpJsonContract("GET", "/api/v1/reticulum/interfaces", INTERFACES_LIST_SCHEMA),
+ HttpJsonContract(
+ "GET",
+ "/api/v1/reticulum/interface-modules",
+ {
+ "type": "object",
+ "required": ["interfacepath", "modules"],
+ "properties": {
+ "interfacepath": {"type": "string"},
+ "modules": {"type": "array"},
+ },
+ "additionalProperties": True,
+ },
+ ),
HttpJsonContract(
"GET",
"/api/v1/community-interfaces",
diff --git a/tests/backend/test_interface_module_store.py b/tests/backend/test_interface_module_store.py
new file mode 100644
index 00000000..1bff125c
--- /dev/null
+++ b/tests/backend/test_interface_module_store.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: 0BSD
+
+import os
+
+import pytest
+
+from meshchatx.src.backend.interface_module_store import (
+ delete_interface_module,
+ install_interface_module,
+ list_interface_modules,
+ sanitize_interface_module_stem,
+ validate_interface_module_source,
+)
+
+_VALID_SRC = b"""# example
+class ExampleInterface:
+ pass
+
+interface_class = ExampleInterface
+"""
+
+
+def test_sanitize_interface_module_stem():
+ assert sanitize_interface_module_stem("WeaveInterface.py") == "WeaveInterface"
+ assert sanitize_interface_module_stem("WeaveInterface") == "WeaveInterface"
+ assert sanitize_interface_module_stem("../evil.py") is None
+ assert sanitize_interface_module_stem("bad-name.py") is None
+ assert sanitize_interface_module_stem("") is None
+
+
+def test_validate_interface_module_source_requires_interface_class():
+ assert validate_interface_module_source(b"class Foo: pass") is not None
+ assert validate_interface_module_source(_VALID_SRC) is None
+ assert validate_interface_module_source(b"") is not None
+ assert validate_interface_module_source(b"\x00interface_class") is not None
+
+
+def test_install_list_delete_interface_module(tmp_path):
+ config_dir = tmp_path / "reticulum"
+ config_dir.mkdir()
+ result = install_interface_module(
+ str(config_dir),
+ filename="ExampleInterface.py",
+ data=_VALID_SRC,
+ )
+ assert result["type"] == "ExampleInterface"
+ assert os.path.isfile(result["path"])
+ listed = list_interface_modules(str(config_dir))
+ assert listed["interfacepath"].endswith("interfaces")
+ assert any(m["type"] == "ExampleInterface" for m in listed["modules"])
+ with pytest.raises(ValueError, match="already exists"):
+ install_interface_module(
+ str(config_dir),
+ filename="ExampleInterface.py",
+ data=_VALID_SRC,
+ overwrite=False,
+ )
+ install_interface_module(
+ str(config_dir),
+ filename="ExampleInterface.py",
+ data=_VALID_SRC + b"\n# updated\n",
+ overwrite=True,
+ )
+ deleted = delete_interface_module(str(config_dir), "ExampleInterface")
+ assert deleted["filename"] == "ExampleInterface.py"
+ assert not os.path.exists(os.path.join(str(config_dir), "interfaces", "ExampleInterface.py"))
diff --git a/tests/frontend/AddInterfaceOptions.test.js b/tests/frontend/AddInterfaceOptions.test.js
index 03ea8b92..cc0b9a72 100644
--- a/tests/frontend/AddInterfaceOptions.test.js
+++ b/tests/frontend/AddInterfaceOptions.test.js
@@ -43,6 +43,14 @@ describe("AddInterfacePage.vue interface options", () => {
if (String(url).includes("/api/v1/reticulum/instance")) {
return { data: { instance: { enable_transport: true } } };
}
+ if (String(url).includes("/api/v1/reticulum/interface-modules")) {
+ return {
+ data: {
+ interfacepath: "/tmp/meshchatx/reticulum/interfaces",
+ modules: [{ type: "ExampleInterface", filename: "ExampleInterface.py", size: 12 }],
+ },
+ };
+ }
if (String(url).includes("/api/v1/reticulum/interfaces")) {
return { data: { interfaces: {} } };
}
@@ -51,6 +59,17 @@ describe("AddInterfacePage.vue interface options", () => {
mockAxios.post.mockResolvedValue({ data: { message: "ok" } });
});
+ it("loads installed interface modules for custom external type", async () => {
+ const wrapper = mountPage();
+ wrapper.vm.newInterfaceType = "__external__";
+ await wrapper.vm.$nextTick();
+ await wrapper.vm.loadInstalledInterfaceModules();
+ expect(mockAxios.get).toHaveBeenCalledWith("/api/v1/reticulum/interface-modules");
+ expect(wrapper.vm.interfaceModulesPath).toContain("interfaces");
+ expect(wrapper.vm.installedInterfaceModules).toHaveLength(1);
+ expect(wrapper.text()).toContain("interfaces.custom_external_install_button");
+ });
+
it("sends AutoInterface group/discovery/data port settings", async () => {
const wrapper = mountPage();
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────